﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace NDF.XnaLibrary.Models
{
	/// <summary>
	/// Class used for animating models using interpolation between keyframes.
	/// </summary>
	public class Animation
	{
		#region Fields

		private SkinnedModelInfo mModelInfo;
		private Matrix[] mBoneTransforms;
		private Matrix[] mAbsoluteTransforms;
		private Matrix[] mSkinTransforms;

		private TimeSpan mCurrentTime;
		private int mCurrentFrameIndex;
		private Matrix[] mFrame1Transforms, mFrame2Transforms;

		#endregion
		
		/// <summary>
		/// Initializes a new animation object.
		/// </summary>
		/// <param name="frames"></param>
		public Animation(AnimationFrameCollection frames)
		{
			Frames = frames;
		}

		#region Properties

		/// <summary>
		/// Gets the total duration the animation.
		/// </summary>
		public TimeSpan Duration
		{
			get { return Frames[Frames.Count - 1].Time; }
		}

		/// <summary>
		/// Gets  the frames defining this animation
		/// </summary>
		public AnimationFrameCollection Frames { get; private set; }

		/// <summary>
		/// Gets the time passed since the beginning of the animation.
		/// </summary>
		public TimeSpan CurrentTime
		{
			get { return mCurrentTime; }
		}

		/// <summary>
		/// Gets the bone transformations relative to their parent bones.
		/// </summary>
		public Matrix[] RelativeBoneTransformations
		{
			get { return mBoneTransforms; }
		}

		/// <summary>
		/// Gets the absolute bone transformations.
		/// </summary>
		public Matrix[] AbsoluteBoneTransformations
		{
			get { return mAbsoluteTransforms; }
		}
		
		/// <summary>
		/// Gets the final bone transformations for usage by the appropriate effect.
		/// </summary>
		public Matrix[] FinalBoneTransformations
		{
			get { return mSkinTransforms; }
		}

		public static Animation FrozenAnimation
		{ 
			get 
			{ 
				AnimationFrameCollection frames = new AnimationFrameCollection();
				frames.Add(new AnimationFrame(TimeSpan.Zero, new List<BoneTransformation>()));
				frames.Add(new AnimationFrame(TimeSpan.MaxValue, new List<BoneTransformation>()));
				Animation anim = new Animation(frames);
				return anim;
			}
		}

		public static Animation BasicMovingAnimation
		{
			get
			{
				AnimationFrameCollection frames = new AnimationFrameCollection();
				frames.Add(new AnimationFrame(TimeSpan.Zero, new List<BoneTransformation>()));
				frames.Add(new AnimationFrame(new TimeSpan(0, 0, 5), new List<BoneTransformation>()));
				frames[1].BoneTransformations.Add(new BoneTransformation(ModelBoneIndex.Neck, Matrix.CreateRotationX(MathHelper.PiOver2)));
				Animation anim = new Animation(frames);
				return anim;
			}
		}

		public static Animation BasicCastingAnimation
		{
			get 
			{
				AnimationFrameCollection frames = new AnimationFrameCollection();
				frames.Add(new AnimationFrame(TimeSpan.Zero, new List<BoneTransformation>()));
				frames.Add(new AnimationFrame(new TimeSpan(0, 0, 2), new List<BoneTransformation>()));
				frames[1].BoneTransformations.Add(new BoneTransformation(ModelBoneIndex.L_Hand, Matrix.CreateRotationZ(MathHelper.PiOver4)));
				frames[1].BoneTransformations.Add(new BoneTransformation(ModelBoneIndex.R_Hand, Matrix.CreateRotationZ(MathHelper.PiOver4)));
				Animation anim = new Animation(frames);
				return anim;
			}
		}

		#endregion

		#region Methods

		/// <summary>
		/// Starts the animation on the given model. In order to animate another model, you must create another animation object.
		/// </summary>
		/// <param name="modelInfo">The mSkinnedModelInfo object of the model to be animated.</param>
		public void Start(SkinnedModelInfo modelInfo)
		{
			mModelInfo = modelInfo;

			mBoneTransforms = new Matrix[modelInfo.BindPose.Count];
			mAbsoluteTransforms = new Matrix[modelInfo.BindPose.Count];
			mSkinTransforms = new Matrix[modelInfo.BindPose.Count];

			Reset();
		}

		/// <summary>
		/// Resets the animation from it`s beginning.
		/// </summary>
		public void Reset()
		{
			if (mModelInfo == null)
			{
				throw new InvalidOperationException("No model has been loaded yet, nothing to reset");
			}

			mCurrentTime = TimeSpan.Zero;
			mCurrentFrameIndex = 0;
			mModelInfo.BindPose.CopyTo(mBoneTransforms);
			UpdateCurrentFrames();
		}

		/// <summary>
		/// Updates the animation.
		/// </summary>
		/// <param name="worldMatrix">The world matrix of the model.</param>
		/// <param name="timeSinceLastUpdate">The time that has passed since the last call to update.</param>
		public void Update(TimeSpan timeSinceLastUpdate)
		{
			mCurrentTime += timeSinceLastUpdate;

			if (mCurrentTime > this.Duration)
			{
				Reset();
			}


			if (mCurrentTime > Frames[mCurrentFrameIndex + 1].Time)
			{
				mCurrentFrameIndex++;
				UpdateCurrentFrames();
			}

			UpdateBoneTransforms();
			UpdateAbsoluteTransforms();
			UpdateSkinTransforms();
		}

		private void UpdateCurrentFrames()
		{
			mFrame1Transforms = Frames[mCurrentFrameIndex].GetFullModelBoneTransformations(mModelInfo);
			mFrame2Transforms = Frames[mCurrentFrameIndex + 1].GetFullModelBoneTransformations(mModelInfo);
		}

		private void UpdateBoneTransforms()
		{
			float timePassedSinceCurrentFrame = (float)(mCurrentTime - Frames[mCurrentFrameIndex].Time).TotalSeconds;
			float totalTimeBetweenFrames = (float)(Frames[mCurrentFrameIndex + 1].Time - Frames[mCurrentFrameIndex].Time).TotalSeconds;
			float timePercentagePassed = timePassedSinceCurrentFrame / totalTimeBetweenFrames;
			for (int bone = 0; bone < mBoneTransforms.Length; bone++)
			{
				mBoneTransforms[bone] = Matrix.Lerp(mFrame1Transforms[bone], mFrame2Transforms[bone], timePercentagePassed);
			}
		}

		private void UpdateAbsoluteTransforms()
		{
			mAbsoluteTransforms[0] = mBoneTransforms[0];

			for (int bone = 1; bone < mAbsoluteTransforms.Length; bone++)
			{
				int parentBone = mModelInfo.SkeletonHierarchy[bone];

				mAbsoluteTransforms[bone] = mBoneTransforms[bone] *
											mAbsoluteTransforms[parentBone];
			}
		}

		private void UpdateSkinTransforms()
		{
			for (int bone = 0; bone < mSkinTransforms.Length; bone++)
			{
				mSkinTransforms[bone] = mModelInfo.InverseBindPose[bone] *
											mAbsoluteTransforms[bone];
			}
		}	

		#endregion
	}
}
